Skip to content

feat(zed): add Zed Hosted AI provider support - #2823

Open
AMSeify wants to merge 7 commits into
decolua:masterfrom
AMSeify:feat/zed-hosted-ai-provider
Open

feat(zed): add Zed Hosted AI provider support#2823
AMSeify wants to merge 7 commits into
decolua:masterfrom
AMSeify:feat/zed-hosted-ai-provider

Conversation

@AMSeify

@AMSeify AMSeify commented Jul 25, 2026

Copy link
Copy Markdown

Summary

  • Add Zed Hosted AI (cloud.zed.dev) as an OAuth provider: registry, executor, OpenAI↔Zed translators, and live model catalog
  • Wire dashboard auth (browser RSA OAuth + credential import), LLM token mint via zedAuth, and MITM CLI capture for Zed credentials
  • Fix /completions 500: restore snake_case CompletionBody.provider wire tags (anthropic, open_ai, google, x_ai) — PascalCase values parse but fail at runtime
  • Add ZedOAuthWrapper (browser vs import), OAuth session/callback fixes, plan-aware empty-catalog messages, and unit tests (tests/unit/zed-constants.test.js)
  • Synced with upstream master v0.5.55
  • Quota: show Zed plan / edit-prediction usage on /dashboard/quota (also feat(usage): show Zed plan quota on the dashboard #3407)

Closes #2821
Related: #3406, #3407

cc @decolua

Test plan

  • Connect a Zed account from the provider dashboard (browser OAuth and manual import)
  • Zed provider page loads live /models catalog (student/Pro accounts)
  • Send a /v1/chat/completions request through a Zed model and confirm streaming works
  • cd tests && npx vitest run unit/zed-constants.test.js unit/zed-usage.test.js
  • OAuth URL baseline: node tests/__baseline__/verify-oauth-urls.mjs
  • Optional: exercise MITM capture path for Zed CLI traffic
  • Open /dashboard/quota with a connected Zed account and confirm plan + edit-prediction rows

Wire Zed cloud.zed.dev into the gateway so users can import Zed Editor credentials, mint/refresh LLM tokens, and route OpenAI-compatible chat through Zed's hosted models. Closes decolua#2821.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment on lines +16 to +18
if (/(claude|anthropic)/i.test(m)) return "anthropic";
if (/(gemini|google)/i.test(m)) return "google";
if (/(grok|x[_-]?ai)/i.test(m)) return "x_ai";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded model-routing regex — should use config constants.
Per open-sse/AGENTS.md : "NEVER hardcode values, models, or block/role strings — use config/ + schema/ constants."
These provider-detection rules should live in open-sse/config/ (e.g. a zedProviderPatterns map in the Zed provider config), not inline in the translator.

Comment thread open-sse/executors/zed.js Outdated
Comment on lines +55 to +103
async refreshCredentials(credentials, log, proxyOptions = null) {
const psd = credentials?.providerSpecificData || {};
const userId = psd.userId;
const zedAccessToken = psd.zedAccessToken || credentials?.refreshToken;
const organizationId = psd.organizationId;
if (!userId || !zedAccessToken) {
log?.warn?.("TOKEN_REFRESH", "Zed missing userId/zedAccessToken for LLM token refresh");
return null;
}
try {
const base = (this.config.baseUrl || "https://cloud.zed.dev").replace(/\/$/, "");
const path = PROVIDER_OAUTH.zed?.llmTokensPath || "/client/llm_tokens";
const body = organizationId ? { organization_id: organizationId } : {};
const res = await proxyAwareFetch(`${base}${path}`, {
method: "POST",
headers: {
Authorization: `${userId} ${zedAccessToken}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(body),
}, proxyOptions);
const text = await res.text();
if (!res.ok) {
log?.error?.("TOKEN_REFRESH", `Zed LLM token refresh failed (${res.status}): ${text.slice(0, 200)}`);
return null;
}
const data = JSON.parse(text);
const raw = data?.token;
const token =
typeof raw === "string"
? raw
: raw && typeof raw === "object"
? raw["0"] || raw.token || Object.values(raw)[0]
: null;
if (!token) return null;
return {
accessToken: token,
expiresIn: 3600,
providerSpecificData: {
llmToken: token,
lastLlmTokenAt: new Date().toISOString(),
},
};
} catch (err) {
log?.error?.("TOKEN_REFRESH", `Zed LLM token refresh failed: ${err.message}`);
return null;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated token refresh logic
ZedExecutor.refreshCredentials() and refreshZedToken() in tokenRefresh/providers.js contain near-identical logic:

  1. Same HTTP call to POST /client/llm_tokens
  2. Same Authorization header format (${userId} ${zedAccessToken})
  3. Same CBOR-ish token unwrap (raw["0"] || raw.token || Object.values(raw)[0])

A bug fix to one won't propagate to the other. Recommendation: have ZedExecutor.refreshCredentials() delegate to refreshZedToken() from tokenRefresh/providers.js, or extract the shared HTTP+unwrap logic into ZedService.refreshLlmToken() (which already exists in src/lib/oauth/services/zed.js:164) and call it from both places.

Comment thread open-sse/executors/zed.js Outdated
// OpenAI chat.completion.chunk
if (event.choices?.[0]) {
const choice = event.choices[0];
const delta = choice.delta || choice.message || {};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing tool_call/tool_use handling — breaks agent workflows
The response parser handles choices.delta, Responses API, Gemini, and Anthropic content shapes, but has no branch for tool_calls or tool_use content blocks.

When a Zed-hosted model returns tool calls (Claude tool_use, OpenAI tool_calls), they'll be silently dropped. This breaks multi-turn agent conversations (Claude Code, Codex, Cline, etc.) that rely on tool-call round-trips.

At minimum, pass through tool_calls/tool_use deltas unchanged so downstream handlers can process them. Other executors (cursor, kiro) handle this — worth checking their implementations for the right pattern.

Comment thread open-sse/providers/registry/index.js
Move model→provider patterns into zedConstants, delegate executor token
refresh to refreshZedToken, pass through/convert tool_calls/tool_use in
the JSONL→SSE parser, and regenerate registry/index.js.

Co-authored-by: Cursor <cursoragent@cursor.com>
@AMSeify

AMSeify commented Jul 25, 2026

Copy link
Copy Markdown
Author

Addressed review feedback in 1e714e4:

  1. Model-routing regex → moved to open-sse/config/zedConstants.js (ZED_PROVIDER_PATTERNS / resolveZedProvider)
  2. Duplicated token refreshZedExecutor.refreshCredentials() now delegates to shared refreshZedToken() (also accepts proxyOptions)
  3. tool_calls / tool_use → JSONL→SSE/JSON parser now passes through OpenAI tool_calls and converts Anthropic tool_use, Gemini functionCall, and Responses API function-call events
  4. registry/index.js → regenerated from the registry directory (alphabetical static import list)

@Diba-k

Diba-k commented Jul 25, 2026

Copy link
Copy Markdown

it works finely without any problem
Screenshot From 2026-07-25 19-41-21

Upstream landed RSA native-app Zed auth (zedAuth, OAuthModal, executor).
Keep complementary CLI import/MITM, route provider patterns through
zedConstants, unhide Zed in the registry, and align import credentials
with the long-lived user-token shape expected by zedLlmFetch.
@AMSeify

AMSeify commented Aug 2, 2026

Copy link
Copy Markdown
Author

Synced this branch with upstream master (now mergeable).

Context: Upstream already landed Zed via RSA native-app OAuth (open-sse/shared/zedAuth.js, OAuthModal, multi-format executor). This PR is now aligned on top of that.

What this PR still adds / adjusts:

  1. Provider routing constants — model→Zed upstream provider tags live in open-sse/config/zedConstants.js (addresses earlier review on hardcoded regex)
  2. Unhide Zed in the registry so it appears in the providers list
  3. CLI import + MITM kept as complementary auth paths; import now stores the long-lived user token (same shape as RSA OAuth) so zedLlmFetch mints LLM tokens on demand
  4. Dashboard Connect uses upstream OAuthModal (RSA flow) instead of the import-only modal
  5. Token refresh documents that LLM bearer refresh happens inside zedAuth (no duplicated HTTP refresh path)

Ready for another look @sunba91-su.

AMSeify and others added 4 commits August 5, 2026 10:55
PascalCase CompletionBody.provider values caused opaque /completions 500s;
restore HTTP wire tags (anthropic/open_ai) and align headers with the working
executor path. Add ZedOAuthWrapper, live model catalog, OAuth callback fixes,
plan-aware empty-catalog messages, and unit tests for the wire protocol.

Co-authored-by: Cursor <cursoragent@cursor.com>
Register a /client/users/me handler so connected Zed accounts appear on
/dashboard/quota with edit-prediction usage, plan labels, and unlimited rows.

Co-authored-by: Cursor <cursoragent@cursor.com>
@AMSeify

AMSeify commented Aug 18, 2026

Copy link
Copy Markdown
Author

Synced this branch with upstream master at v0.5.55 (# v0.5.55 (2026-08-14)). Merge is clean again.

Also stacked Zed quota tracking (feat(usage): show Zed plan quota on the dashboard) so connected Zed accounts show up on /dashboard/quota. That work is also in a standalone PR against current master: #3407 (issue #3406).

Ready for another look @decolua @sunba91-su.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add Zed Hosted AI provider support

3 participants